package statusMachine

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/apache/rocketmq-client-go/v2/consumer"
	"github.com/apache/rocketmq-client-go/v2/primitive"

	"wowCoupon.com/logger"
	"wowCoupon.com/models"
	"wowCoupon.com/mq"
	"wowCoupon.com/pkg/e"
)

func init() {
	MqProducer.Start()
	StatusMachineInstance.Start()
}

var (
	MqAdmin    = mq.NewAdmin(NameServerAddrs, BrokerAddr, Topic, ProducerGroup)
	MqProducer = mq.NewProducer(NameServerAddrs, ProducerGroup, Topic)
	MqConsumer = mq.NewConsumer(NameServerAddrs, ConsumerGroup, Topic)
)

var (
	NameServerAddrs []string = []string{"192.168.31.129:9876"}
	Topic           string   = "CouponValidity"
	BrokerAddr      string   = "192.168.31.129:10911"
	ProducerGroup   string   = "CouponValidityPG1"
	ConsumerGroup   string   = "CouponValidityCG1"

	// delayTime and delayLevel are const, never change, it is safe for concurrency, lock is not needed
	delayTime  []int64       = []int64{1, 5, 10, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 1200, 1800, 3600, 7200}
	delayLevel map[int64]int = map[int64]int{
		1:    1,
		5:    2,
		10:   3,
		30:   4,
		60:   5,
		120:  6,
		180:  7,
		240:  8,
		300:  9,
		360:  10,
		420:  11,
		480:  12,
		540:  13,
		600:  14,
		1200: 15,
		1800: 16,
		3600: 17,
		7200: 18,
	}
)

var StatusMachineInstance *StatusMachine = NewStatusMachine(Topic)

// Monitor is an automatic and independent module, it is called by main program
// when the main program executes. It's used for monitoring all CouponMetas' status
// both in Mysql and Redis, and then load or sync data between Mysql and Redis.
// The Mysql is used for persistence as well as the Redis is used for seckill
type StatusMachine struct {
	// The mq topic which stores each coupon's status message (coupon basic info with delay time)
	StatusTopic string
	// The callback function which is called by mq consumer to handle a coupon's status
	CouponStatusHandler func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error)
}

func NewStatusMachine(statusTopic string) *StatusMachine {
	s := &StatusMachine{
		StatusTopic: statusTopic,
	}

	s.CouponStatusHandler = func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
		for _, msg := range msgs {
			couponValiditySlice := strings.Split(string(msg.Body), ":")
			couponID, _ := strconv.ParseInt(couponValiditySlice[0], 10, 64)
			expireTimeStamp, _ := strconv.ParseInt(couponValiditySlice[1], 10, 64)

			if err := s.addCouponValidity(couponID, expireTimeStamp); err != nil {
				fmt.Errorf(err.Error())
			}

			//fmt.Printf("consume msg: %v\n", msg)

		}

		return consumer.ConsumeSuccess, nil
	}

	return s
}

// Check if a coupon is expired or not
func (s *StatusMachine) isCouponExpired(expireTimeStamp int64) bool {
	if expireTimeStamp <= time.Now().Unix() {
		return true
	}

	return false
}

func (s *StatusMachine) getFirstSmallerDelayTime(expireTime int64) int64 {
	var smallerTime int64 = 1
	for _, t := range delayTime {
		if t > expireTime {
			break
		}

		smallerTime = t
	}

	return smallerTime
}

func (s *StatusMachine) getDelayTimeLevel(delayTime int64) int {
	return delayLevel[delayTime]
}

// Update coupon's status to expired in mysql or redis
func (s *StatusMachine) updateCouponStatusExpired(couponID int64) error {
	// update mysql
	var couponMeta *models.CouponMeta
	var err error

	if couponMeta, err = models.GetCouponMetaByID(couponID); err != nil {
		logger.Error("StatusMachine updateCouponStatusExpired failed: %s", err.Error())
		return err
	}

	couponMeta.Status = e.COUPON_STATUS_END
	if err := models.UpdateCouponMetaStatus(couponMeta.ID, couponMeta.Status); err != nil {
		logger.Error("StatusMachine updateCouponStatusExpired failed: %s", err.Error())
		return err
	}

	logger.Info("StatusMachine -> MySql: Coupon meta %d status has been updated to expired", couponID)

	// update redis
	// to-do
	return nil
}

// Start the status machine, do prepare job
func (s *StatusMachine) Start() error {
	// Mq admin create status topic
	if s.StatusTopic == "" {
		logger.Error("StatusMachine starts failed: topic should not be empty string")
		return nil
	}

	if err := MqAdmin.CreateTopic(s.StatusTopic); err != nil {
		logger.Error("StatusMachine starts failed: create topic err: %s", err.Error())
		return err
	}

	logger.Info("StatusMachine starts successfully")
	return nil
}

// Add coupon's validity info to message queue as a delay message
func (s *StatusMachine) addCouponValidity(couponID int64, expireTimeStamp int64) error {
	// Coupon is expired, update database then return
	if s.isCouponExpired(expireTimeStamp) {
		return s.updateCouponStatusExpired(couponID)
	}

	// generate message
	message := fmt.Sprint(couponID) + ":" + fmt.Sprint(expireTimeStamp)

	// how long to expiration
	expireTime := expireTimeStamp - time.Now().Unix()

	// get first smaller or equal delay time level
	delayLevel := s.getDelayTimeLevel(s.getFirstSmallerDelayTime(expireTime))

	// produce delay message
	if err := MqProducer.SendSyncDelayMessage([]byte(message), delayLevel); err != nil {
		return err
	}

	return nil
}

// Consume each delay message of coupon's validity
func (s *StatusMachine) cycleCheckCouponValidity() error {
	if err := MqConsumer.ConsumeMessage(s.CouponStatusHandler); err != nil {
		return err
	}

	return nil
}

// Open Api
func AddCouponValidity(couponID int64, expireTimeStamp int64) error {
	return StatusMachineInstance.addCouponValidity(couponID, expireTimeStamp)
}

func CycleCheckCouponValidity() error {
	return StatusMachineInstance.cycleCheckCouponValidity()
}
